package fi.helsinki.cs.ohtu10k.schedule;

import org.joda.time.DateTime;
import org.joda.time.Duration;
import java.util.Set;
import java.util.SortedSet;
import java.util.HashSet;
import java.util.TreeSet;
import java.io.Serializable;

/**
 * Manager class for courses and events.
 * @author Thomas Nyman
 * @author Jani Rönkä
 *
 */
public class PeriodManager implements ScheduleManager, Serializable {
	/**
	 * Serial version ID used for serialization. 
	 */
	private static final long serialVersionUID = -412198464763580315L;
	
	/**
	 * Set holding managed courses.
	 */
	private Set<ScheduleCourse> coursetable;
	
	/**
	 * Set holding manager events.
	 */
	private SortedSet<ScheduleEvent> timetable; 
	
	/**
	 * Constructs a new empty PeriodManager. 
	 */
	PeriodManager() {
		coursetable = new HashSet<ScheduleCourse>();
		timetable = new TreeSet<ScheduleEvent>();
	}
	
	/**
	 * Returns <code>true</code> if no courses of events are associated with the manager.
	 * @return <code>true</code> if no courses of events are associated with the manager.
	 */
	public boolean isEmpty() {
		return (timetable.isEmpty() && coursetable.isEmpty());
	}

	/**
	 * Returns array of events with start date between specified start date and end date.
	 * @param startDate Starting bound of search.
	 * @param endDate 	End bound of search.
	 * @return Array of events with start date between startDate and endDate.
	 */
	@Override
	public ScheduleEvent[] getEvents(DateTime startDate, DateTime endDate) {
		ScheduleEvent head = null;
		ScheduleEvent tail = null;
		for (ScheduleEvent e : timetable) {
			DateTime eventStartDate = e.getStartDate();
			DateTime eventEndDate = e.getStartDate().withDurationAdded(e.getDuration(), 1);
			// Find starting element
			if (head == null 
				&& (eventStartDate.isBefore(endDate) || eventStartDate.equals(endDate)) 
				&& ((eventEndDate.isAfter(startDate) || eventEndDate.equals(startDate)))) {
				head = e;
			}
			// Find ending element
			if (tail == null && eventStartDate.isAfter(endDate)) {
				tail = e;
			}
		}
	 
		if (head != null && tail == null) { // If we have a head, but no tail return elements from head
			return timetable.tailSet(head).toArray(new ScheduleEvent[0]);
		} else if (head == null) { 			// If no head return empty array
			return new ScheduleEvent[0];
		}
		
		return timetable.subSet(head, tail).toArray(new ScheduleEvent[0]);
	}
	
	/**
	 * Returns array of courses associated with the manager.
	 * @return Array of courses associated with the manager.
	 */
	@Override
	public ScheduleCourse[] getCourses() {
		return coursetable.toArray(new ScheduleCourse[0]);
	}

	/**
	 * Adds new course to manager with specified attributes.
	 * @param startDate Start Date of course.
	 * @param endDate	End date of course.
	 * @param name		Name of course 
	 * @param abbr		Abbreviation of course.
	 * @throws IllegalArgumentException
	 */
	@Override
	public void addCourse(DateTime startDate, DateTime endDate, String name, String abbr) 
		throws IllegalArgumentException {
		coursetable.add(ScheduleCourseFactory.getScheduleCourse(startDate, endDate, name, abbr));
	}
	
	/**
	 * Removes specified course from manager if present.
	 * All events associated to specified course are removed as well.
	 * @param course Course to remove.
	 * @throws IllegalArgumentException If course null or not found. 
	 */
	@Override
	public void delCourse(ScheduleCourse course) throws IllegalArgumentException {
		
		if (course == null || !coursetable.remove(course))
			throw new IllegalArgumentException("Specified course not found");
		
		// Remove all event associated to course.
		for (ScheduleEvent e: course.getEvents()) {
			timetable.remove(e);
		}
	}
	
	/**
	 * Returns <code>true</code> if manager contains specified course, else <code>false</code>. 
	 * @param course Course to search for.
	 * @return <code>true</code> if manager contains specified course, else <code>false</code>.
	 * @throws IllegalArgumentException If specified course is null.
	 */
	public boolean containsCourse(ScheduleCourse course) throws IllegalArgumentException {
		if (course == null) throw new IllegalArgumentException("Event specified null");
		return coursetable.contains(course);
	}
	
	/**
	 * Adds new event to manager with specified attributes. 
	 * @param startDate		Start date of event.
	 * @param duration		Duration of event.
	 * @param name			Name of event.
	 * @param course		Course event belongs to, null if none
	 * @param isNotifiable	<code>true</code> if user should be notified of this event, otherwise <code>false</code>.
	 * @throws IllegalArgumentException If event creation fails.
	 * @throws TimeSlotTakenException	If time slot is already in use.
	 */
	@Override
	public void addEvent(DateTime startDate, Duration duration, String name, 
						 ScheduleCourse course, boolean isNotifiable) throws IllegalArgumentException, 
						 TimeSlotTakenException {
		ScheduleEvent event = ScheduleEventFactory.getScheduleEvent(startDate, duration, name, course, isNotifiable);
		
		DateTime endDate = startDate.withDurationAdded( duration, 1 );
		ScheduleEvent[] conflicting = getEvents( startDate, endDate );
		if( conflicting.length != 0 ) {
			for( ScheduleEvent e : conflicting ) {
				if ( !( endDate.equals( e.getStartDate() ) ||
						event.getStartDate().equals( e.getStartDate().withDurationAdded( e.getDuration(), 1 ) ) ) )
					throw new TimeSlotTakenException( "Time slot taken by " + e );
						            
			}
		}
		
		if (course != null) course.addEvent(event);
		timetable.add(event);
	}
	
	/**
	 * Adds new event to manager with specified attributes. 
	 * @param startDate		Start date of event as DateTime.
	 * @param duration		Duration of event as Duration.
	 * @param name			Name of event as String.
	 * @param isNotifiable	<code>true</code> if user should be notified of this event, otherwise <code>false</code>.
	 * @throws IllegalArgumentException If event creation fails.
	 * @throws TimeSlotTakenException	If time slot is already in use.
	 */
	@Override
	public void addEvent(DateTime startDate, Duration duration, String name,
						 boolean isNotifiable) throws IllegalArgumentException, 
						 TimeSlotTakenException {
		addEvent(startDate, duration, name, null, isNotifiable);
	}
	
	/**
	 * Removes specified event from manager if present.
	 * @param event	Event to be removed.
	 * @throws IllegalArgumentException If event <code>null</code> or not found.
	 */
	@Override
	public void delEvent(ScheduleEvent event) {
		
		if (event == null || !timetable.remove(event)) {
			throw new IllegalArgumentException("Specified event not found");
		}
		// Remove association of removed event from its course.
		if (event.getCourse() != null) {
			event.getCourse().delEvent(event);
		}
	}
	
	/**
	 * Returns <code>true</code> if manager contains specified event, else <code>false</code>. 
	 * @param course Course to search for.
	 * @return <code>true</code> if manager contains specified event, else <code>false</code>.
	 * @throws IllegalArgumentException If specified event is <code>null</code>.
	 */
	public boolean containsEvent(ScheduleEvent event) throws IllegalArgumentException {
		if (event == null) throw new IllegalArgumentException("Event specified null");
		return timetable.contains(event);
	}

}
